package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"os/signal"
	"sort"
	"strconv"
	"strings"
	"syscall"
)

type (
	echoResult struct {
		Method  string          `json:"method,omitempty"`
		Path    string          `json:"path,omitempty"`
		Query   []*keyValuePair `json:"query,omitempty"`
		Headers []*keyValuePair `json:"headers,omitempty"`
		Length  uint64          `json:"length,omitempty"`
		Type    string          `json:"type,omitempty"`
		Body    any             `json:"body,omitempty"`
	}

	keyValuePair struct {
		Key   string `json:"key,omitempty"`
		Value string `json:"value,omitempty"`
	}
)

const (
	AppName    = "echo"
	AppVersion = "0.0.1"

	DefaultHost = ""
	DefaultPort = 10086
)

var (
	flagVersion bool
	flagHost    string
	flagPort    uint
)

func init() {
	flag.BoolVar(&flagVersion, "version", false, "Show version message")
	flag.StringVar(&flagHost, "host", DefaultHost, "Host name")
	flag.UintVar(&flagPort, "port", DefaultPort, "Port")

	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "Usage: %s -version\n", AppName)
		fmt.Fprintf(os.Stderr, "Usage: %s -h\n", AppName)
		fmt.Fprintf(os.Stderr, "Usage: %s [-host HOST] [-port PORT]\n", AppName)
		flag.PrintDefaults()
	}
}

func main() {
	flag.Parse()

	if flagVersion {
		fmt.Printf("%s [%s]", AppName, AppVersion)
		return
	}

	go doServe()

	// 启动守护routine。
	sigChannel := make(chan os.Signal, 256)
	signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
	for {
		sig := <-sigChannel
		fmt.Fprintf(os.Stderr, "Received sig: %#v\n", sig)
		switch sig {
		case syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM:
			return
		}
	}
}

func doServe() {
	http.HandleFunc("/", handleFunc)

	addr := flagHost + ":" + strconv.FormatUint(uint64(flagPort), 10)
	server := &http.Server{
		Addr:           addr,
		Handler:        nil,
		ReadTimeout:    0,
		WriteTimeout:   0,
		MaxHeaderBytes: 0,
	}
	fmt.Printf("Serving @ %s\n", server.Addr)

	server.ListenAndServe()
}

func handleFunc(w http.ResponseWriter, r *http.Request) {
	var result echoResult

	result.Method = r.Method
	result.Path = r.URL.Path
	result.Headers = encodeValues(r.Header)
	result.Query = encodeValues(r.URL.Query())
	result.Type = strings.ToLower(r.Header.Get("Content-Type"))
	result.Length = uint64(r.ContentLength)

	var contentType string
	ss := strings.SplitN(result.Type, ";", 2)
	if len(ss) > 1 {
		contentType = strings.ToLower(ss[0])
	} else {
		contentType = result.Type
	}

	switch contentType {
	case "application/x-www-form-urlencoded":
		if err := r.ParseForm(); err != nil {
			writeError(w, err)
			return
		} else {
			result.Body = encodeValues(r.Form)
		}
	case "multipart/form-data":
		if err := r.ParseMultipartForm(1024 * 1024 * 100); err != nil {
			writeError(w, err)
			return
		} else {
			f0 := encodeValues(r.MultipartForm.Value)
			f1 := encodeFormFiles(r.MultipartForm.File)
			if len(f1) > 0 {
				f0 = append(f0, f1...)
			}
			result.Body = f0
		}
	case "application/json":
		if bs, err := io.ReadAll(r.Body); err != nil {
			writeError(w, err)
			return
		} else {
			result.Body = string(bs)
		}
	}

	if bd, err := json.Marshal(result); err != nil {
		writeError(w, err)
		return
	} else {
		writeResult(w, bd)
	}
}

func encodeValues(vs map[string][]string) []*keyValuePair {
	if vs == nil {
		return nil
	}

	result := make([]*keyValuePair, 0, len(vs))

	keys := make([]string, 0, len(vs))
	for k := range vs {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		v := vs[k]
		if len(v) > 0 {
			result = append(result, &keyValuePair{k, strings.Join(v, ",")})
		}
	}

	return result
}
func encodeFormFiles(vs map[string][]*multipart.FileHeader) []*keyValuePair {
	if vs == nil {
		return nil
	}

	result := make([]*keyValuePair, 0, len(vs))

	keys := make([]string, 0, len(vs))
	for k := range vs {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		vs := vs[k]
		if len(vs) > 0 {
			fa := make([]string, 0, len(vs))
			for _, v := range vs {
				fa = append(fa, fmt.Sprintf("(Name=%s,Size=%d)", v.Filename, v.Size))
			}
			if len(fa) > 0 {
				result = append(result, &keyValuePair{k, strings.Join(fa, ",")})
			}
		}
	}

	return result
}

func writeError(w http.ResponseWriter, err error) {
	w.WriteHeader(http.StatusInternalServerError)
	w.Write([]byte(err.Error()))
}

func writeResult(w http.ResponseWriter, data []byte) {
	w.Header().Add("Content-Type", "application/json")
	w.Header().Add("Content-Length", fmt.Sprintf("%d", len(data)))
	w.WriteHeader(http.StatusOK)
	w.Write(data)
}
